<?php
/**
 * Copyright (c) 2006-2007, Julien PORTALIER
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright    Copyright (c) 2006-2007, Julien PORTALIER
 * @link         http://featherphp.googlecode.com/
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */

/**
 * Framework's Database related Model.
 */
abstract class ActiveRecord extends Model
{
	public    $dbname;
	public    $schema;
	
	// DB
	public    $db;
	protected $dbConfig = 'default';
	protected $prefix;
	protected $useTable;
	public    $table;
	public    $_table;
	public    $fields;
	public    $primaryKey = 'id';
	public    $foreignKey;
	
	// queries
	public    $defaultJoin      = 'LEFT';
	protected $defaultProcess   = Q_PROCESS_FULL;
	public    $process          = Q_PROCESS_FULL;
	
	public    $recursiveProcess = Q_PROCESS_LITE;
	public    $recursiveFields  = '*';
	protected $cascadeDeletion  = false;
	
	public    $defaultFields    = '*';
	public    $defaultOrder     = '';
	protected $defaultOperator  = 'AND';
	
	public    $_join;
	
	protected $_joins;
	protected $_operator;
	protected $_conditions  = array();
	protected $_order       = '';
	protected $_groupby     = '';
	protected $_limit       = '';
	
	
	function __construct(Controller $controller=null)
	{
		parent::__construct($controller);
		
		$this->db = Database::get($this->dbConfig);
		
		# table
		if (empty($this->useTable))
		{
			$this->table      = $this->db->tablePrefix.$this->prefix.Inflector::tableize($this->name);
			$this->foreignKey = Inflector::underscore($this->name).'_'.$this->primaryKey;
		}
		else
		{
			$this->table      = $this->db->tablePrefix.$this->prefix.$this->useTable;
			$this->foreignKey = Inflector::singularize($this->useTable).'_'.$this->primaryKey;
		}
		
		# schema?
		if (!empty($this->schema))
			$dbtable = "{$this->schema}.{$this->table}";
		elseif (!empty($this->db->schema))
			$dbtable = "{$this->db->schema}.{$this->table}";
		else
			$dbtable = $this->table;
		
		$_table = $dbtable;
		$dbtable = $this->db->field($dbtable);
		
		# fields (desc)
		/*
		if (function_exists('apc_fetch'))
		{
			# APC cache
			$this->fields = apc_fetch('models'.DS.$this->table);
			
			if (empty($this->fields))
			{
				$this->fields = $this->db->describeTable($dbtable);
				apc_store('models'.DS.$this->table, $this->fields, strtotime(Core::cache_lifetime));
			}
		}
		else
		{
			# File cache
			$this->fields = cache('models'.DS.$this->table);
			
			if (!empty($this->fields))
				$this->fields = unserialize($this->fields);
			else
			{
				$this->fields = $this->db->describeTable($dbtable);
				cache('models'.DS.$this->table, serialize($this->fields));
			}
		}
		*/
		$this->fields = cache('models'.DS.$this->table, null, DEBUG ? '+30 seconds' : '+1 hour', 'apc');
		if (empty($this->fields))
		{
			$this->fields = $this->db->describeTable($dbtable);
			cache('models'.DS.$this->table, $this->fields, DEBUG ? '+30 seconds' : '+1 hour', 'apc');
		}
		
		# vars
		$this->_table = $_table;
		$this->table  = $dbtable;
		$this->dbname = $this->db->field($this->name);
		
		# defaults
		$this->process    = $this->defaultProcess;
		$this->_join      = $this->defaultJoin;
		$this->_operator  = $this->defaultOperator;
	}
	
	function & __call($func, $args)
	{
		preg_match('/(find(|All))By([\w\d]+)/', $func, &$match);
		if (!empty($match))
		{
			$this->conditions(array(
				$this->name.'.'.strtolower($match[3]) => array_shift($args)
			));
			$rs = call_user_func_array(array($this, $match[1]), $args);
			return $rs;
		}
		trigger_error(sprintf('Unknown method %s->%s', $this->name, $func));
	}
	
	// queries
	
	/**
	 * Checks if a record exists.
	 * 
	 * @param  $id
	 * @return boolean True if record exists.
	 */
	function exists($id=null, $recursive=-1)
	{
		$this->condition('id', $id ? $id : $this->id);
		return $this->findCount($recursive) ? true : false;
	}
	
	/**
	 * Adds a single condition
	 * 
	 * @param $field
	 * @param $value
	 */
	function condition($field, $value)
	{
		$this->conditions(array($field => $value));
	}
	
	/**
	 * Adds one or more conditions
	 * 
	 * @param $conditions
	 */
	function conditions($conditions)
	{
		$this->_conditions = array_merge($this->_conditions, $conditions);
	}
	
	/**
	 * Sets the operator
	 * 
	 * @param $operator Can be 'AND' or 'OR'.
	 */
	function operator($operator)
	{
		$this->_operator = $operator;
	}
	
	/**
	 * @deprecated Because not necessary
	 */
	function fields($fields)
	{
		$this->_fields   = $this->db->fields($fields);
	}
	
	/**
	 * Sets order for sorting records.
	 * 
	 * @param $order Eg: 'Model.created DESC'
	 */
	function order($order)
	{
		$this->_order = $this->db->order($order);
	}
	
	/**
	 * Groups records.
	 * 
	 * @param $groupby Eg: 'Model.type'
	 */
	function groupby($groupby)
	{
		$this->_groupby = $this->db->groupby($groupby);
	}
	
	/**
	 * Groups records.
	 * 
	 * @param $limit
	 * @param $page
	 */
	function limit($limit, $page=null)
	{
		$this->_limit = $this->db->limit($limit, $page);
	}
	
	/**
	 * Permits to join tables. Bindings should be prefered over joins.
	 * 
	 * @param $left  Name of left model.
	 * @param $right Name of right model.
	 */
	function join($left, $right)
	{
		if (is_string($left))
			$left = $this->load->model($left);
		if (is_string($right))
			$right = $this->load->model($right);
		$this->_joins .= " {$this->_join} JOIN {$left->table} AS ".$this->db->field($left->name)." ON ".
			$this->db->field("{$left->name}.{$right->primaryKey}")." = ".
			$this->db->field("{$right->name}.{$left->foreignKey}");
	}
	
	/**
	 * Finds many records.
	 * 
	 * @param  $fields
	 * @param  $limit
	 * @param  $page
	 * @param  $recursive
	 * @return array
	 */
	function & findAll($fields=null, $limit=null, $page=null, $recursive=null)
	{
		if (!is_null($recursive))
			$this->recursive = $recursive;
		
		$this->beforeFind();
		
		# joins
		$joins = '';
		if ($this->recursive > -1)
		{
			foreach($this->_belongsTo as $j)
			{
				$joins .= " {$this->_join} JOIN {$this->$j->table} AS ".$this->db->field($j)." ON ".
					$this->db->field("$j.{$this->$j->primaryKey}")." = ".
					$this->db->field("{$this->name}.{$this->$j->foreignKey}");
			}
			foreach($this->_hasOne as $j)
			{
				$joins .= " {$this->_join} JOIN {$this->$j->table} AS ".$this->db->field($j)." ON ".
					$this->db->field("$j.{$this->foreignKey}")." = ".
					$this->db->field("{$this->name}.{$this->primaryKey}");
			}
		}
		$joins .= $this->_joins;
		
		# limit
		if (isset($limit) or isset($page))
			$this->_limit = $this->db->limit($limit, $page);
		
		# fields
		if (empty($fields))
			$fields = empty($this->_fields) ? $this->defaultFields : $this->_fields;
		$_fields = $this->db->fields($fields);
		
		# query
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		$sql        = "SELECT $_fields FROM {$this->table} AS {$this->dbname} $joins " .
		              "$conditions {$this->_groupby} {$this->_order} {$this->_limit} ;";
		$results    = $this->db->query($sql, $this->process, $this->name);
		
		# pagination
		if (isset($page))
		{
			# prepare
			/*
			$pos = strpos($fields, ',');
			if ($pos)
			{
				$_fields = rtrim(substr($fields, 0, $pos));
				if (substr($_fields, -1) == '*')
					$_fields = "{$this->name}.{$this->primaryKey}";
				$fields = $_fields;
			}
			elseif (substr($_fields, -1) == '*')
			{
				var_dump($_fields);
				//$_fields = rtrim($_fields, '*').$this->primaryKey;
				
				if (strpos($_fields, '.'))
					list($model, $field) = 
				
				var_dump($_fields);
			}
			*/
			list($field,) = explode(',', $fields);
			$field = trim($field);
			$pos = strpos($field, '*');
			
			if ($pos === 0)
				$field = "{$this->name}.{$this->primaryKey}";
			elseif ($pos !== false and strpos($field, '.'))
			{
				list($model,) = explode('.', $field);
				$field = ($model == $this->name) ? $this->primaryKey : $this->$model->primaryKey;
				$field = "$model.$field";
			}
			
			$recursive = min(0, $this->recursive);
			
			# count
			$order   = $this->_order;   $this->_order   = null;
			$groupby = $this->_groupby; $this->_groupby = null;
			
			$results = new ArrayObject($results);
			$results->_paginate = array(
				'limit' => $limit,
				'page'  => $page,
				'count' => $this->findCount($recursive, false, "DISTINCT $field")
			);
			
			$this->_order   = $order;
			$this->_groupby = $groupby;
		}
		
		# recursive?
		if ($this->recursive > 0 and $this->process == Q_PROCESS_FULL and !empty($this->_hasMany) and !empty($results))
		{
			foreach($this->_hasMany as $a)
			{
				# prepares
				if (!empty($this->$a->defaultOrder))
					$this->$a->order($this->$a->defaultOrder);
				
				$this->$a->reset   = false;
				$this->$a->process = $this->$a->recursiveProcess;
				$recursive = isset($this->$a->_recursive) ? $this->$a->_recursive : -1;
				
				# queries
				foreach($results as $index => $row)
				{
					$this->$a->conditions(array("$a.{$this->foreignKey}" => $row[$this->name][$this->primaryKey]));
					$results[$index][$a] = $this->$a->findAll($this->$a->recursiveFields, null, null, $recursive);
				}
				
				# cleans up
				$this->$a->__reset();
				$this->$a->reset = true;
			}
		}
		
		# reset
		if ($this->reset)
			$this->__reset();
		
		$this->afterFind();
		return $results;
	}
	
	/**
	 * Internal function to reset conditions, bindings, etc. 
	 */
	protected function __reset()
	{
		if (isset($this->_modifiedModels))
		{
			$this->_hasMany   = $this->hasMany;
			$this->_hasOne    = $this->hasOne;
			$this->_belongsTo = $this->belongsTo;
			$this->_modifiedModels = null;
		}
		$this->recursive   = $this->defaultRecursive;
		$this->_operator   = $this->defaultOperator;
		$this->_join       = $this->defaultJoin;
		$this->_joins      = '';
		$this->_order      = '';
		$this->_groupby    = '';
		$this->_limit      = '';
		$this->_conditions = array();
		$this->process     = $this->defaultProcess;
	}
	
	/**
	 * Reads a record.
	 * 
	 * @param  $fields
	 * @param  $id
	 * @param  $recursive
	 * @return array
	 */
	function & read($fields='*', $id=null, $recursive=0)
	{
		if (empty($this->_conditions) and (isset($id) or isset($this->id)))
			$this->conditions(array("{$this->name}.id" => is_null($id) ? $this->id : $id));
		$rs = $this->find($fields, $recursive);
		return $rs;
	}
	
	/**
	 * Reads a field's value in a record.
	 * 
	 * @return mixed Field's value or false.
	 */
	function field($field, $id=null, $recursive=-1)
	{
		$this->process = Q_PROCESS_NONE;
		$rs = $this->read($field, $id, $recursive);
		return empty($rs) ? false : $rs[0];
	}
	
	/**
	 * Counts records (correspond to the pre given conditions)
	 * 
	 * @param  $recursive
	 * @param  $reset
	 * @param  $field Checks for a specific field
	 * @return integer Number of records
	 */
	function findCount($recursive=0, $reset=true, $field=null)
	{
		$_recursive = $this->recursive; $this->recursive = $recursive;
		$_reset     = $this->reset;     $this->reset     = $reset;
		$_process   = $this->process;   $this->process   = Q_PROCESS_NONE;
		
#		if (empty($field))
#			$field = '*';
#		elseif (strpos($field, 'DISTINCT') === false)
#			$field = "DISTINCT $field";
		
#		$field = empty($field) ? '*' : $this->db->field("DISTINCT $field");
		$field = empty($field) ? '*' : $this->db->field("$field");
		$rs = $this->find("-! COUNT($field)");
		
		$this->reset     = $_reset;
		$this->process   = $_process;
		$this->recursive = $_recursive;
		return empty($rs) ? false : $rs[0];
	}
	
	/**
	 * Finds neighbours to a record, from a field/value point of view.
	 * 
	 * @param $field  The field to check (eg: 'created')
	 * @param $value  The value to check
	 * @param $fields The list of fields needed (defaults to all)
	 */
	function & findNeighbours($field, $value, $fields='*', $recursive=1)
	{
		$rs = array();
		
		$this->reset = false;
		$rs['previous'] = $this->findPrevious($field, $value, $fields, $recursive);
		
		$this->reset = true;
		$rs['next'] = $this->findNext($field, $value, $fields, $recursive);
		
		return $rs;
	}
	
	/**
	 * Finds the next neighbour to a record, from a field/value point of view.
	 * 
	 * @param $field  The field to check (eg: 'created')
	 * @param $value  The value to check
	 * @param $fields The list of fields needed (defaults to all)
	 */
	function & findNext($field, $value, $fields='*', $recursive=1)
	{
		$this->order("$field ASC");
		$this->_conditions[" $field "] = "> $value";
		return $this->find($fields, $recursive);
	}
	
	/**
	 * Finds the previous neighbour to a record, from a field/value point of
	 * view.
	 * 
	 * @param $field  The field to check (eg: 'created')
	 * @param $value  The value to check
	 * @param $fields The list of fields needed (defaults to all)
	 */
	function & findPrevious($field, $value, $fields='*', $recursive=1)
	{
		$this->order("$field DESC");
		$this->_conditions[" $field "] = "< $value";
		return $this->find($fields, $recursive);
	}
	
	/**
	 * Generates a list of values for a specific field.
	 * 
	 * @param  $field
	 * @param  $recursive
	 * @return array      An array of values. 
	 */
	function & listField($field, $recursive=null)
	{
		$this->process = Q_PROCESS_LIST;
		$rs = $this->findAll("DISTINCT $field", null, null, $recursive);
		return $rs;
	}
	
	/**
	 * Generates a list of associated values from two fields : $one => $two.
	 * 
	 * @param  $fieldkey
	 * @param  $fieldValue
	 * @param  $recursive
	 * @return array      An array of values. 
	 */
	function & listFieldAssoc($fieldKey, $fieldValue, $recursive=null)
	{
		$this->process = Q_PROCESS_ASSOC_LIST;
		return $this->findAll("$fieldKey, $fieldValue", null, null, $recursive);
	}
	
	
	// inserts & updates
	
	/**
	 * Checks if a field's value is unique. Mostly used for data validation.
	 * 
	 * @param  $field  Field to check
	 * @param  $value  Value to check
	 * @return boolean True if a record was found
	 */
	protected function validateUnique($field, $value)
	{
		if (is_null($this->id))
		{
			$this->conditions(array($field => $value));
			return (false !== $this->field($field, null, -1));
		}
		return false;
	}
	
	/**
	 * Saves a single record (internal method).
	 * 
	 * @param  $type   Either 'insert', 'update' or 'replace'
	 * @return boolean True if save was successful
	 */
	protected function __save($type=null)
	{
		$data =& $this->data[$this->name];
		
		# id
		if (isset($data[$this->primaryKey]))
			$this->id = $data[$this->primaryKey];
		
		if (!empty($this->id) and $type != 'insert' and $type != 'replace')
		{
			# update
			if (array_key_exists('modified', $this->fields) and empty($data['modified']))
				$data['modified'] = date('Y-m-d H:i:s');
			
			$updates = array();
			
			foreach($data as $f => $v)
			{
				if (array_key_exists($f, $this->fields))
					array_push($updates, $this->db->field($f)." = ".$this->db->value($v));
			}
			
			$sql = "UPDATE {$this->table} SET ".implode(', ', $updates).
				" WHERE ".$this->db->field($this->primaryKey)." = ".$this->db->value($this->id)." ;";
			
			if (!$this->db->execute($sql))
				return false;
		}
		else
		{
			# insert or replace
			if (array_key_exists('created', $this->fields) and empty($data['created']))
				$data['created'] = date('Y-m-d H:i:s');
			
			$fields = array();
			$values = array();
			
			foreach($data as $f => $v)
			{
				if (array_key_exists($f, $this->fields))
				{
					array_push($fields, $this->db->field($f));
					array_push($values, $this->db->value($v));
				}
			}
			$fields = implode(', ', $fields);
			$values = implode(', ', $values);
			
			if ($type == 'replace')
				$sql = "REPLACE INTO {$this->table} ( $fields ) VALUES ( $values ) ;";
			else
				$sql = "INSERT INTO {$this->table} ( $fields ) VALUES ( $values ) ;";
			
			if (!$this->db->execute($sql))
				return false;
			
			# new id
			if (empty($this->id))
				$this->id = $this->db->getLastInsertID();
			if (!isset($this->data[$this->name][$this->primaryKey]))
			 	$this->data[$this->name][$this->primaryKey] = $this->id;
		}
		
		return true;
	}
	
	/**
	 * Saves a single field in a single record.
	 * 
	 * @param  $field    The field to save
	 * @param  $value    The value to save
	 * @param  $validate 
	 * @return boolean   True if save was successful
	 */
	function saveField($field, $value, $validate=false)
	{
		return $this->saveFields(array($field => $value), $validate);
	}
	
	/**
	 * Saves multiple fields at once for a single record.
	 * 
	 * @param  $data     Hash {'field' => 'value'}
	 * @param  $validate 
	 * @return boolean   True if save was successful
	 */
	function saveFields(array $data, $validate=false)
	{
		if ($validate and !$this->validates())
			return false;
		
		// conditions
		if (empty($this->_conditions))
		{
			if (is_null($this->id))
				return false;
			$this->conditions(array($this->primaryKey => $this->id));
		}
		
		// data
		$fields = array();
		foreach ($data as $field => $value)
		{
			array_push($fields,
				$this->db->field($field).' = '.$this->db->value($value));
		}
		$fields = implode(', ', $fields);
		
		// save
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		$rs = $this->db->execute("UPDATE {$this->table} SET $fields $conditions ;");
		
		// reset
		$this->__reset();
		return $rs;
	}
	
	/**
	 * Saves many records at once.
	 * 
	 * @param  $fields List of field to save data for
	 * @param  $values List of values to save
	 * @return boolean insertion was successful
	 */
	function saveMany($fields, &$values)
	{
		if (empty($values))
			return true;
		$fields = $this->db->fields($fields);
		
		// values
		foreach (array_keys($values) as $k)
		{
			foreach(array_keys($values[$k]) as $n) {
				$values[$k][$n] = $this->db->value($values[$k][$n]);
			}
			$values[$k] = '( '.implode(', ', $values[$k]).' ) ';
		}
		$values = implode(', ', $values);
		
		// save
		return $this->db->execute("INSERT INTO {$this->table} ( $fields ) VALUES $values ;");
	}
	
	
	// deletion
	
	/**
	 * Deletes a single record.
	 * 
	 * @param  $id
	 * @param  $cascade Cascade deletion?
	 * @return boolean  True if deletion was successful
	 */
	function delete($id=null, $cascade=false)
	{
		if (empty($id))
			$id = $this->id;
		
		if (!$this->beforeDelete())
			return false;
		
		// cascade deletion?
		if ($this->cascadeDeletion or $cascade)
		{
			foreach ($this->__getRelated() as $Model)
			{
				if (!$this->$Model->deleteCascade($this->foreignKey, $id))
					return false;
			}
		}
		
		// deletes itself
		if (!$this->__delete($id))
			return false;
		
		return $this->afterDelete();
	}
	
	/**
	 * @todo TEST & DEBUG!
	 */
	function deleteAll(array $conditions=null)
	{
		return;
		
		if (!empty($conditions))
			$this->conditions(&$conditions);
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		
		return $this->db->execute("DELETE FROM {$this->table} $conditions;");
	}
	
	/**
	 * Deletes data in cascade. Automatically called from parent's
	 * Model::delete() method.
	 * 
	 * @deprecated
	 * 
	 * @param  $foreignKey Parent's model foreign key
	 * @param  $foreignId  Parent's data id
	 * @return boolean     True if cascade deletion was successful
	 */
	function deleteCascade($foreignKey, $foreignId)
	{
		$this->conditions($foreignKey, $foreignId);
		$list = $this->listField('id', 0);
		
		foreach($list as $id)
		{
			if (!$this->delete($id))
				return false;
		}
		
		return true;
	}
	
	/**
	 * Internal function to delete data.
	 * 
	 * @param  $id
	 * @return boolean True if deletion was successful
	 */
	protected function __delete($id=null)
	{
		# conditions
		if (!empty($id))
			$this->condition('id', $id);
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		
		# deletes
		$rs = $this->db->execute("DELETE FROM {$this->table} $conditions;");
		
		# finishes
		$this->__reset();
		return $rs;
	}
	
	// callbacks
	
	/**
	 * Automatic callback called before each find.
	 */
	function beforeFind()
	{
	}
	
	/**
	 * Automatic callback called after each find.
	 */
	function afterFind()
	{
	}
}
?>
